﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using uTILLIty.Outlook.FlexFileAddIn.Properties;
using OL = Microsoft.Office.Interop.Outlook;

namespace uTILLIty.Outlook.FlexFileAddIn
{
	public class MapiFolderManager
	{
		private readonly List<string> _blacklistedFolderIds = new List<string>(100);

		private readonly List<string> _includedStoreIds = new List<string>(10);
		private OL.Application _application;

		public MapiFolderManager()
		{
			InitializeList();
		}

		public SynchronizedBindingList<FolderInfo> FolderList { get; private set; }

		public bool FullLoadInProgress { get; protected set; }

		public event EventHandler<EventArgs<string>> LoadStatusChanged;

		private void RaiseLoadStatusChanged(string status, params object[] args)
		{
			var msg = string.Format(status, args ?? new object[0]);
#if DEBUG
			Debug.WriteLine(msg);
#endif

			if (LoadStatusChanged == null)
				return;

			LoadStatusChanged.Invoke(this, new EventArgs<string>(msg));
		}

		public string[] GetIncludedStoreIds()
		{
			return _includedStoreIds.ToArray();
		}

		public string[] GetBlacklistedFolderIds()
		{
			return _blacklistedFolderIds.ToArray();
		}

		public void BeginLoadingFolders()
		{
			if (_application == null)
				throw new InvalidOperationException("Call overload BeginLoadingFolders(OL.Application application) first!");

			InitializeList();
			Task.Factory.StartNew(LoadFolders);
		}

		private void InitializeList()
		{
			if (FolderList == null)
				FolderList = new SynchronizedBindingList<FolderInfo>();
			else
				FolderList.Clear();
		}

		public void BeginLoadingFolders(OL.Application application)
		{
			_application = application;
			var folders = application.Session.Folders;
			folders.FolderAdd += OnFolderAdded;
			folders.FolderChange += OnFolderChanged;
			folders.FolderRemove += OnFolderRemoved;
			BeginLoadingFolders();
		}

		private void OnFolderAdded(OL.MAPIFolder folder)
		{
			Debug.WriteLine($"Folder {folder.Name} was added");
		}

		private void OnFolderChanged(OL.MAPIFolder folder)
		{
			Debug.WriteLine($"Folder {folder.Name} was changed");
		}

		private void OnFolderRemoved()
		{
			Debug.WriteLine("Folder was removed");
		}

		private void LoadFolders()
		{
			var folders = _application.Session.Folders;
			//var query = "http://schemas.microsoft.com/mapi/foldername:%";
			//var query = "name:%";
			//var search = _application.AdvancedSearch(string.Empty, query, true);
			//foreach (var result in search.Results)
			//{
			//}
			try
			{
				FullLoadInProgress = true;
				RaiseLoadStatusChanged(Resources.LoadingFolders, folders.Count);
				foreach (OL.MAPIFolder folder in folders)
				{
					var storeInfo = new StoreInfo(folder.Store);
					ProcessFolder(folder, null, storeInfo);
				}
				RaiseLoadStatusChanged(Resources.LoadingCompleted, FolderList.Count);
			}
			catch (Exception ex)
			{
				Debug.WriteLine(ex.ToString());
				RaiseLoadStatusChanged("Error processing the stores. {0}", ex.Message);
			}
			finally
			{
				FullLoadInProgress = false;
			}
		}

		private void LoadFoldersByStore()
		{
			var tasks = new List<Task>(10);
			var lcts = new LimitedConcurrencyLevelTaskScheduler(Settings.Default.MaxParallelLoadingFolders);
			var factory = new TaskFactory(lcts);

			try
			{
				FullLoadInProgress = true;
				foreach (OL.Store store in _application.Session.Stores)
				{
					switch (store.ExchangeStoreType)
					{
						case OL.OlExchangeStoreType.olPrimaryExchangeMailbox: //added as the primary account
						case OL.OlExchangeStoreType.olAdditionalExchangeMailbox: //added as a separate account
						case OL.OlExchangeStoreType.olExchangeMailbox: //included in another mailbox
							var list = BuildBlacklistedFoldersList(store);
							_blacklistedFolderIds.AddRange(list);
							break;
					}
				}

				var sortedStores = _application.Session.Stores.Cast<OL.Store>()
					.OrderBy(s => s.ExchangeStoreType, new ExchangeStoreTypeComparer());
				foreach (var store in sortedStores)
				{
#if DEBUG
					RaiseLoadStatusChanged(Resources.StoreDetected, store.DisplayName, store.ExchangeStoreType);
#endif

					switch (store.ExchangeStoreType)
					{
						//case OL.OlExchangeStoreType.olPrimaryExchangeMailbox: //added as the primary account
						//case OL.OlExchangeStoreType.olAdditionalExchangeMailbox: //added as a separate account
						//case OL.OlExchangeStoreType.olExchangePublicFolder: //slow, if many folders
						//case OL.OlExchangeStoreType.olExchangeMailbox: //included in another mailbox
						case OL.OlExchangeStoreType.olNotExchange: //ICAL, Sharepoint, etc.
							continue;
					}
					_includedStoreIds.Add(store.StoreID);

					var task = factory.StartNew(ProcessStore, store);
					tasks.Add(task);
				}
			}
			catch (Exception ex)
			{
				RaiseLoadStatusChanged("Error processing the stores. {0}", ex);
			}
			finally
			{
				if (tasks.Count > 0)
					factory.ContinueWhenAll(tasks.ToArray(), t => FullLoadInProgress = false);
			}
		}

		private IEnumerable<string> BuildBlacklistedFoldersList(OL.Store store)
		{
			var blacklist = new List<string>();

			TryGetFolderId(store, OL.OlDefaultFolders.olFolderDeletedItems, blacklist.Add);
			TryGetFolderId(store, OL.OlDefaultFolders.olFolderDrafts, blacklist.Add);
			TryGetFolderId(store, OL.OlDefaultFolders.olFolderInbox, blacklist.Add);
			TryGetFolderId(store, OL.OlDefaultFolders.olFolderJunk, blacklist.Add);
			TryGetFolderId(store, OL.OlDefaultFolders.olFolderLocalFailures, blacklist.Add);
			TryGetFolderId(store, OL.OlDefaultFolders.olFolderOutbox, blacklist.Add);
			TryGetFolderId(store, OL.OlDefaultFolders.olFolderServerFailures, blacklist.Add);
			TryGetFolderId(store, OL.OlDefaultFolders.olFolderSentMail, blacklist.Add);
			TryGetFolderId(store, OL.OlDefaultFolders.olFolderSyncIssues, blacklist.Add);
			TryGetFolderId(store, OL.OlDefaultFolders.olPublicFoldersAllPublicFolders, blacklist.Add);

			return blacklist.ToArray();
		}

		private void TryGetFolderId(OL.Store store, OL.OlDefaultFolders folderType, Action<string> action)
		{
			try
			{
				var folder = store.GetDefaultFolder(folderType);
				if (folder != null)
					action(folder.EntryID);
			}
			catch (COMException)
			{
				RaiseLoadStatusChanged(Resources.DefaultFolderNotFound, folderType, store.DisplayName);
				/*
				 *	System.Runtime.InteropServices.COMException was unhandled by user code
						HResult=-2147221233
						Message=Der Ordner "Lokale Fehler" kann nicht gefunden werden.
						Source=Microsoft Outlook
						ErrorCode=-2147221233
				 * */
			}
		}

		private void ProcessStore(object state)
		{
			var store = (OL.Store) state;
			var stopwatch = new Stopwatch();
			RaiseLoadStatusChanged(Resources.LoadingFoldersFromStore, store.DisplayName);

			stopwatch.Start();
			var rootFolder = store.GetRootFolder() as OL.Folder;
			if (rootFolder == null)
				return;

			var storeInfo = new StoreInfo(store);

			try
			{
				foreach (OL.MAPIFolder folder in rootFolder.Folders)
				{
					ProcessFolder(folder, null, storeInfo);
					Thread.Sleep(10);
				}
			}
			catch (COMException)
			{
				/*
					System.Runtime.InteropServices.COMException was unhandled by user code
						HResult=-1836842723
						Message=Microsoft Exchange ist nicht verfügbar. Es bestehen Netzwerkprobleme, oder der Servercomputer mit Exchange wurde für Wartungsarbeiten heruntergefahren.
						Source=Microsoft Outlook
						ErrorCode=-1836842723
				 * 
					System.Runtime.InteropServices.COMException was unhandled by user code
						HResult=-1836842723
						Message=Microsoft Exchange ist nicht verfügbar. Es bestehen Netzwerkprobleme, oder der Servercomputer mit Exchange wurde für Wartungsarbeiten heruntergefahren.
						Source=Microsoft Outlook
						ErrorCode=-1836842723
				 * 
					System.Runtime.InteropServices.COMException was unhandled by user code
						HResult=-1698430691
						Message=Microsoft Exchange ist nicht verfügbar. Es bestehen Netzwerkprobleme, oder der Servercomputer mit Exchange wurde für Wartungsarbeiten heruntergefahren.
						Source=Microsoft Outlook
						ErrorCode=-1698430691
				 * * */
			}
			catch (InvalidOperationException)
			{
				/* enumeration changed */
			}
			stopwatch.Stop();

			var folderCount = -1;
			while (folderCount == -1)
			{
				try
				{
					folderCount = FolderList.Count(i => i.Store.StoreId.Equals(storeInfo.StoreId));
				}
				catch (InvalidOperationException)
				{
				}
			}
			RaiseLoadStatusChanged(Resources.LoadingFromStoreCompleted, folderCount, store.DisplayName,
				stopwatch.Elapsed.TotalSeconds);
		}

		private void ProcessFolder(OL.MAPIFolder folder, FolderInfo parent, StoreInfo store)
		{
			var fi = parent;

			switch (folder.DefaultItemType)
			{
				case OL.OlItemType.olMailItem:
					//case OL.OlItemType.olAppointmentItem:
					//case OL.OlItemType.olContactItem:
					//case OL.OlItemType.olTaskItem:
					//case OL.OlItemType.olJournalItem:
					//case OL.OlItemType.olNoteItem:
					//case OL.OlItemType.olPostItem:
					//case OL.OlItemType.olDistributionListItem:
					//case OL.OlItemType.olMobileItemSMS:
					//case OL.OlItemType.olMobileItemMMS:
					fi = CreateFolderInfo(folder, parent, store);
					if (FolderList.SynchronizationContext != null &&
					    FolderList.SynchronizationContext != SynchronizationContext.Current)
					{
						//only lock list, if we are in a different thread then the UI thread
						lock (FolderList)
						{
							AddFolderIfNotBlacklisted(fi);
						}
					}
					else
					{
						AddFolderIfNotBlacklisted(fi);
					}
					break;
			}

			foreach (OL.MAPIFolder subFolder in folder.Folders)
			{
				ProcessFolder(subFolder, fi, store);
				Thread.Sleep(10);
			}
		}

		private void AddFolderIfNotBlacklisted(FolderInfo fi)
		{
			if (_blacklistedFolderIds.Contains(fi.FolderId))
				return;
			if (!FolderList.Contains(fi))
				FolderList.Add(fi);
		}

		private FolderInfo CreateFolderInfo(OL.MAPIFolder folder, FolderInfo parent, StoreInfo store)
		{
			var fi = new FolderInfo(folder, parent, store);
			return fi;
		}

		public OL.MAPIFolder GetMapiFolder(FolderInfo folder)
		{
			var path = new List<string>(10);
			var curFolder = folder;
			while (curFolder != null)
			{
				path.Add(curFolder.Name);
				curFolder = curFolder.Parent;
			}
			path.Reverse();

			var store = _application.Session.GetStoreFromID(folder.Store.StoreId);

			if (string.Equals(path[0], store.DisplayName, StringComparison.CurrentCultureIgnoreCase))
				path.RemoveAt(0);

			var curMapiFolder = store.GetRootFolder();

			foreach (var folderName in path)
			{
				try
				{
					curMapiFolder = curMapiFolder.Folders[folderName];
					if (curMapiFolder == null) //should never occur (COMException raised), but just to make sure
						return null;
				}
				catch (COMException)
				{
					//item not found in collection
					return null;
				}
			}

			return curMapiFolder;
		}
	}
}